﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using PartialExpressionCompleter.Util;
using Microsoft.Cci;
using Microsoft.Cci.Immutable;

namespace PartialExpressionCompleter.Query
{
    class UnknownMethod : Expression
    {
        private readonly List<Expression> args;
        public UnknownMethod(List<Expression> args)
        {
            this.args = args;
        }

        public override bool IsComplete { get { return false; } }
        public override bool IsNonTrivialQuery { get { return true; } }

        public IEnumerable<Expression> Arguments { get { return args.AsEnumerable(); } }

        protected override IEnumerable<Expression> GetRewritings()
        {
            if (args.Any(a => a.IsNonTrivialQuery))
            {
                throw new NotImplementedException();
            }
            else
            {
                // +1 because of instance methods
                for (int i = 1; i <= CCIUtil.GetMaxParameterCount() + 1; i++)
                {
                    IEnumerable<Expression> unknowns =
                        Enumerable.Repeat<Expression>(UnknownExpression.Instance, i);
                    yield return new UnknownMethod(args.Concat(unknowns).ToList());
                }
            }
        }

        protected static IEnumerable<List<Expression>> AllAssignments(
                List<IParameterDefinition> parameters, List<Expression> args)
        {
            if (args.Count == 0)
            {
                return Enumerable.Repeat<Expression>(UnknownExpression.Instance,
                                                     parameters.Count)
                    .ToList().Singleton();
            }
            else
            {
                return args.Select(a => parameters
                                .Select(p => p.Type.ResolvedType
                                              .IsAssignableFromGeneric(a.Type))
                                              .ToArray())
                                .ToArray()
                                .EnumerateMaximumMatchings()
                                .Select(matching =>
                                    matching.Select(i => i == -1
                                        ? UnknownExpression.Instance
                                        : args[i])
                                    .ToList());
            }
        }
        protected static IEnumerable<List<Expression>> AllAssignments(
            List<ITypeReference> parameters, List<Expression> args)
        {
            if (args.Count == 0)
            {
                return Enumerable.Repeat<Expression>(UnknownExpression.Instance,
                                                     parameters.Count)
                    .ToList().Singleton();
            }
            else
            {
                return args.Select(a => parameters
                                .Select(p => p.ResolvedType
                                              .IsAssignableFromGeneric(a.Type))
                                              .ToArray())
                                .ToArray()
                                .EnumerateMaximumMatchings()
                                .Select(matching =>
                                    matching.Select(i => i == -1
                                        ? UnknownExpression.Instance
                                        : args[i])
                                    .ToList());
            }
        }

        protected override IEnumerable<Expression> GetExactQueryResults()
        {
            var argCount = args.Count;
            // TODO .Type is never Dummy... right?
            var knownTypeArgs = args.Where(a => a.Type != null)
                                    .OrderBy(a => a.Type.ToString())
                                    .ToList();
            foreach (var receiver in knownTypeArgs)
            {
                var rest = knownTypeArgs.ToList();
                rest.Remove(receiver);
                if (rest.Count + 1 != knownTypeArgs.Count)
                {
                    throw new Exception("Doubled argument?: " + receiver);
                }
                foreach (var meth in receiver.Type.ResolvedType
                                             .GetInstanceMethods()
                                             .Where(m => m.ParameterCount == argCount - 1))
                {
                    foreach (var assignment
                        in AllAssignments(meth.Parameters.ToList(), rest))
                    {
                        yield return new MethodCall(meth,
                            receiver.Cons(assignment).ToList(), false);
                    }
                }
            }
            foreach (var staticMeth in CCIUtil.GetAllStaticMethods()
                .Where(m => m.ParameterCount == argCount))
            {
                foreach (var genericArgs
                    in CCIUtil.GetAllTypes()
                              .CartesianProductSingletons(staticMeth.GenericParameterCount))
                {
                    var meth = staticMeth;
                    if (genericArgs.Any())
                    {
                        meth = new GenericMethodInstance(staticMeth,
                            genericArgs.Cast<ITypeReference>(),
                            CCIUtil.GetInternFactory());
                    }

                    foreach (var assignment
                        in AllAssignments(meth.Parameters.ToList(), knownTypeArgs))
                    {
                        yield return new MethodCall(meth, assignment, false);
                    }
                }
            }
        }
        protected IEnumerable<Tuple<IMethodDefinition, Expression>> GetPossibleMethods<E>(
                List<Expression> knownTypeArgs, QueryContext<E> c) where E : IExpression
        {
            var argCount = knownTypeArgs.Count;
            foreach (var receiver in knownTypeArgs)
            {
                var rest = knownTypeArgs.ToList();
                rest.Remove(receiver);
                if (rest.Count + 1 != knownTypeArgs.Count)
                {
                    throw new Exception("Doubled argument?: " + receiver);
                }
                foreach (var meth in receiver.Type.ResolvedType
                                             .GetInstanceMethods()
                                             .Where(m => m.ParameterCount >= argCount - 1))
                {
                    yield return meth.PairWith(receiver);
                }
            }
            foreach (var staticMeth in CCIUtil.GetAllStaticMethods()
                .Where(m => m.ParameterCount >= argCount))
            {
                if (!c.IsMemberVisible(staticMeth))
                {
                    continue;
                }
                yield return staticMeth.PairWith((Expression)null);
            }
        }
        public IEnumerable<Expression> GetUnorderedQueryResultsWithoutCache<E>(QueryContext<E> c) where E:IExpression
        {
            // TODO Actually check access.
            var argCount = args.Count;
            // TODO .Type is never Dummy... right?
            var knownTypeArgs = args.Where(a => a.Type != null)
                                    .OrderBy(a => a.Type.ToString())
                                    .ToList();
            foreach (var tup in GetPossibleMethods(knownTypeArgs, c))
            {
                var meth = tup.Item1;
                var receiver = tup.Item2;
                var isMethInstance = receiver != null;

                var isMethGeneric = meth.GenericParameterCount > 0;
                var isMethParamsGeneric = meth.ContainingType
                        .Cons(meth.Parameters.Select(p => p.Type))
                        .Any(t => t.ContainsGenericParameter());
                var methContainingType = meth.ContainingType.ResolvedType;
                var isTypeGeneric = methContainingType.GenericParameterCount > 0;
                var paramList = meth.GetParameterTypes().ToList();
                // XXX DEBUG
                /*
                if (meth.Name.ToString().Equals("Add"))
                {
                }
                //*/
                foreach (var assignment
                    in AllAssignments(paramList, knownTypeArgs)
                    .Where(a => !isMethInstance || a[0].Type != null))
                {
                    var instantiatedMethod = meth;
                    if (isMethGeneric && !isMethParamsGeneric)
                    {
                    }
                    if (isMethParamsGeneric)
                    {
                        var methGenParamCount = meth.GenericParameterCount;
                        var genericParams = (meth.GenericParameters.Cast<IGenericParameter>()
                                    .Concat(meth.ContainingType.ResolvedType
                                            .GenericParameters.Cast<IGenericParameter>()))
                                .UnifyUsing(paramList,
                                            assignment.Select(a => a.Type).ToList());
                        if (genericParams == null)
                        {
                            instantiatedMethod = null;
                        }
                        else if (isMethGeneric && !isTypeGeneric)
                        {
                            instantiatedMethod = new GenericMethodInstance(meth,
                                genericParams, CCIUtil.GetInternFactory());
                        }
                        else if (isTypeGeneric)
                        {
                            var instantiatedType = GenericTypeInstance
                                .GetGenericTypeInstance(methContainingType as INamedTypeDefinition,
                                                        genericParams.Skip(methGenParamCount),
                                                        CCIUtil.GetInternFactory());
                            // TODO Better way to handle this?
                            var namedMethods = instantiatedType.Methods
                                .Where(m => m.Name.Equals(meth.Name)
                                    && m.ParameterCount == meth.ParameterCount
                                    && m is ISpecializedMethodReference
                                    && (m as ISpecializedMethodReference).UnspecializedVersion.Equals(meth))
                                .ToList();
                            if (namedMethods.Count != 1)
                            {
                                throw new Exception("Method identification not unique enough for: " + meth);
                            }
                            instantiatedMethod = namedMethods[0];
                            if (isMethGeneric)
                            {
                                instantiatedMethod = new GenericMethodInstance(instantiatedMethod,
                                    genericParams.Take(methGenParamCount), CCIUtil.GetInternFactory());
                            }
                        }
                        else
                        {
                            if (!meth.GetParameterTypes().Zip(assignment,
                                (p, a) => a.Type == null
                                    || p.IsAssignableFrom(a.Type, false))
                                .All())
                            {
                                instantiatedMethod = null;
                            }
                        }
                    }
                    if (instantiatedMethod != null)
                    {
                        /*
                        if (instantiatedMethod.Type.Cons(instantiatedMethod.Parameters.Select(p => p.Type))
                                .Any(t => t.ContainsGenericParameter()))
                        {
                            // TODO ... what if it in the definition of a generic method?
                            throw new Exception("Instantiated method has generic parameters: "
                                + instantiatedMethod);
                        }*/
                        var res = new MethodCall(instantiatedMethod,
                                                    assignment, false);
                        yield return res;
                    }
                }
            }
        }

        protected IEnumerable<Expression> GetUnorderedQueryResultsUsingCache<E>(QueryContext<E> c) where E : IExpression
        {
            var argCount = args.Count;
            // TODO .Type is never Dummy... right?
            var knownTypeArgs = args.Where(a => a.Type != null)
                                    .OrderBy(a => a.Type.ToString())
                                    .ToList();
            foreach (var meth in MethodCacheUtil
                .GetSmallestMethodCacheForTypes(knownTypeArgs.Select(a => a.Type))
                .AllStaticMethods
                .Concat(knownTypeArgs.Select(a => a.Type.GetInstanceMethods()).Concat())
                //.Concat(knownTypeArgs.Select(a => MethodCache.GetMethodCacheForType(a.Type).AllInstanceMethods).Concat())
                .Where(m => c.IsMemberVisible(m)))
            {
                var isMethInstance = meth.HasThisParameter();

                var isMethGeneric = meth.GenericParameterCount > 0;
                var isMethParamsGeneric = meth.ContainingType
                        .Cons(meth.Parameters.Select(p => p.Type))
                        .Any(t => t.ContainsGenericParameter());
                var methContainingType = meth.ContainingType.ResolvedType;
                var isTypeGeneric = methContainingType.GenericParameterCount > 0;
                var paramList = meth.GetParameterTypes().ToList();
                // XXX DEBUG
                /*
                if (meth.Name.ToString().Equals("Add"))
                {
                }
                //*/
                foreach (var assignment
                    in AllAssignments(paramList, knownTypeArgs)
                    .Where(a => !isMethInstance || a[0].Type != null))
                {
                    var instantiatedMethod = meth;
                    if (isMethGeneric && !isMethParamsGeneric)
                    {
                    }
                    if (isMethParamsGeneric)
                    {
                        var methGenParamCount = meth.GenericParameterCount;
                        var genericParams = (meth.GenericParameters.Cast<IGenericParameter>()
                                    .Concat(meth.ContainingType.ResolvedType
                                            .GenericParameters.Cast<IGenericParameter>()))
                                .UnifyUsing(paramList,
                                            assignment.Select(a => a.Type).ToList());
                        if (genericParams == null)
                        {
                            instantiatedMethod = null;
                        }
                        else if (isMethGeneric && !isTypeGeneric)
                        {
                            instantiatedMethod = new GenericMethodInstance(meth,
                                genericParams, CCIUtil.GetInternFactory());
                        }
                        else if (isTypeGeneric)
                        {
                            var instantiatedType = GenericTypeInstance
                                .GetGenericTypeInstance(methContainingType as INamedTypeDefinition,
                                                        genericParams.Skip(methGenParamCount),
                                                        CCIUtil.GetInternFactory());
                            // TODO Better way to handle this?
                            var namedMethods = instantiatedType.Methods
                                .Where(m => m.Name.Equals(meth.Name)
                                    && m.ParameterCount == meth.ParameterCount
                                    && m is ISpecializedMethodReference
                                    && (m as ISpecializedMethodReference).UnspecializedVersion.Equals(meth))
                                .ToList();
                            if (namedMethods.Count != 1)
                            {
                                throw new Exception("Method identification not unique enough for: " + meth);
                            }
                            instantiatedMethod = namedMethods[0];
                            if (isMethGeneric)
                            {
                                instantiatedMethod = new GenericMethodInstance(instantiatedMethod,
                                    genericParams.Take(methGenParamCount), CCIUtil.GetInternFactory());
                            }
                        }
                        else
                        {
                            if (!meth.GetParameterTypes().Zip(assignment,
                                (p, a) => a.Type == null
                                    || p.IsAssignableFrom(a.Type, false))
                                .All())
                            {
                                instantiatedMethod = null;
                            }
                        }
                    }
                    if (instantiatedMethod != null)
                    {
                        /*
                        if (instantiatedMethod.Type.Cons(instantiatedMethod.Parameters.Select(p => p.Type))
                                .Any(t => t.ContainsGenericParameter()))
                        {
                            // TODO ... what if it in the definition of a generic method?
                            throw new Exception("Instantiated method has generic parameters: "
                                + instantiatedMethod);
                        }*/
                        if (!instantiatedMethod.GetParameterTypes().Zip(assignment,
                                (p, a) => a.Type == null
                                    || p.IsAssignableFrom(a.Type, false))
                                .All())
                        {
                            instantiatedMethod = null;
                        }
                        else
                        {
                            var res = new MethodCall(instantiatedMethod,
                                                        assignment, false);
                            yield return res;
                        }
                    }
                }
            }
        }

        public override IEnumerable<Expression> GetUnorderedQueryResults<E>(QueryContext<E> c)
        {
            return GetUnorderedQueryResultsUsingCache(c);
        }

        public override ITypeDefinition Type
        {
            get { throw new NotImplementedException(); }
        }

        public override string ToString()
        {
            return "?(" + args.Select(a => "/* " + a.Type + " */ " + a)
                              .CommaSeparated() + ")";
        }

        public override bool IsUserProvided
        {
            get { return true; }
        }
    }
}
